﻿using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Net;
using Tnelab.Common;

namespace Tnelab.WebApp
{
    class InvokeUIServiceInfo
    {
        public string? ServiceName { get; set; }
        public string? MethodName { get; set; }
        public List<object> Arguments { get; set; }=new List<object>();
    }
    public static class WebApplication
    {
        //static WebApplication(){
        //    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        //}
        public static bool DebugEnable { get; set; }= false;
        internal static IWebWindow? MainWindow { get; private set; }
        public static Dictionary<string, object> UIServices { get; }=new Dictionary<string, object>();
        internal static WebAppHttpServer? WebAppServer { get; private set; } = null;
        /// <summary>
        /// 执行消息循环
        /// </summary>
        /// <param name="webBrowser"></param>
        /// <param name="url">
        /// file协议(忽略大小写)：
        ///     以file:///开头，后跟文件绝对路径或相对exe所在路径
        /// http或https协议(忽略大小写)：
        ///     以http开头，直接按照通用http协议处理
        /// 内嵌资源协议(忽略大小写)：
        ///     非http、https、file开头都视为内嵌资源协议
        ///     格式为(忽略大小写)：
        ///     1、/资源所在程序集名称/资源路径(资源路径中的.替换为/) 
        ///         如：/Tnelab.WebAppTestUI/UI/main.html，表示加载程序集Tnelab.WebAppTestUI中ui.main.html资源
        ///     2、资源路径(资源路径中的.替换为/)
        ///         如：UI/main.html，表示加载调用者程序集中ui.main.html资源
        ///         
        /// </param>
        /// <param name="ip"></param>
        /// <param name="port"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public static void Run(IWebBrowser webBrowser,string url,string? ip=null,int port=0)
        {
            if (webBrowser == null)
                throw new ArgumentNullException(nameof(webBrowser));
            if (string.IsNullOrEmpty(url.Trim()))
                throw new ArgumentNullException(nameof(url));

            var fileFlag = "file:///";
            if (url.StartsWith(fileFlag))
            {
                var tmpStr = url.Remove(0, fileFlag.Length);
                if (tmpStr.IndexOf(":") == -1)
                {
                    url = url.Insert(fileFlag.Length, $"{Environment.CurrentDirectory.Replace("\\", "/")}/");
                }
            }
            else
            {
                if (!url.StartsWith("http"))
                {
                    if (ip == null)
                    {
                        ip = "127.0.0.1";
                    }
                    if (port == 0)
                    {
                        var random = new Random(DateTime.Now.Millisecond);
                        port = random.Next(10000, 55535);
                    }
                    WebAppServer = new WebAppHttpServer(ip, port);
                    _ = WebAppServer.StartAsync();
                    var callerMethod = new StackFrame(1).GetMethod();
                    var aname = callerMethod.DeclaringType.Assembly.GetName().Name;
                    if (url[0] != '/')
                    {
                        url = $"http://{ip}:{port}/{aname}/{url}";
                    }
                    else
                    {
                        url = $"http://{ip}:{port}{url}";
                    }
                }
            }

            webBrowser.AddSchemeHandler("use", UseHandler);
            webBrowser.AddSchemeHandler("uiservice", UIServiceHandler);
            webBrowser.AddSchemeHandler("service", RemoteServiceHandler);
            webBrowser.JsCall += WebBrowser_JsCall;
            MainWindow = new WebWindow(null, "MainWindow", webBrowser, url);
            UIServices.Add(MainWindow.Name, MainWindow);
            if (DebugEnable)
            {
                MainWindow.Show();
            }
            else
            {
                MainWindow.Hide();
            }
            Win32Api.MSG msg = new Win32Api.MSG();
            while (Win32Api.GetMessage(ref msg, IntPtr.Zero, 0, 0))
            {
                Win32Api.TranslateMessage(ref msg);
                Win32Api.DispatchMessage(ref msg);
            }
        }
        internal static async Task NewWindow(WebWindow parent,string name,string url)
        {
            if (MainWindow == null)
                throw new InvalidOperationException("MainWindow is null.");

            var webBrowser = Activator.CreateInstance(MainWindow.WebBrowser.GetType()) as IWebBrowser;
            if (webBrowser == null)
            {
                throw new InvalidOperationException("NewWindow时，创建WebBrowser失败。");
            }
            webBrowser.AddSchemeHandler("use", UseHandler);
            webBrowser.AddSchemeHandler("uiservice", UIServiceHandler);
            webBrowser.JsCall += WebBrowser_JsCall;

            var window = new WebWindow(parent, name, webBrowser, url);
            UIServices.Add(window.Name, window);
            if (DebugEnable)
            {
                window.Show();
            }
            else
            {
                window.Hide();                
            }
            using var stream= WebApplication.UIServiceHandler($"UIService://{name}") as MemoryStream;
            var script = Encoding.UTF8.GetString(stream.ToArray(), 0, (int)stream.Length);
            if (parent != null)
            {
                await parent.WebBrowser.CallJsAsync(script);
                async void toExecuteScript(object? sender, EventArgs e)
                {
                    using var stream = WebApplication.UIServiceHandler($"UIService://{parent.Name}") as MemoryStream;
                    var script = Encoding.UTF8.GetString(stream.ToArray(), 0, (int)stream.Length);
                    await window.WebBrowser.CallJsAsync(script);
                    window.WebBrowser.DOMContentLoaded -= toExecuteScript;
                }
                window.WebBrowser.DOMContentLoaded += toExecuteScript;
            }

        }
        private static object? JsonElementToObject(Type type, JsonElement elm)
        {
            object? result = null;
            if (type.IsEnum)
            {
                result = Enum.ToObject(type,elm.GetInt32());
                return result;
            }
            switch (type.Name)
            {
                case "Boolean":
                    result = elm.GetBoolean();
                    break;
                case "Byte":
                    result = elm.GetByte();
                    break;
                case "Int16":
                    result = elm.GetInt16();
                    break;
                case "Int32":
                    result = elm.GetInt32();
                    break;
                case "Int64":
                    result = elm.GetInt64();
                    break;
                case "SByte":
                    result = elm.GetSByte();
                    break;
                case "UInt16":
                    result = elm.GetUInt16();
                    break;
                case "UInt32":
                    result = elm.GetUInt32();
                    break;
                case "UInt64":
                    result = elm.GetUInt64();
                    break;

                case "Single":
                    result = elm.GetSingle();
                    break;
                case "Double":
                    result = elm.GetDouble();
                    break;
                case "Decimal":
                    result = elm.GetDecimal();
                    break;
                case "String":
                    result = elm.GetString();
                    break;
            }
            return result;
        }        
        private static async void WebBrowser_JsCall(object? sender, JsCallEventArgs e)
        {
            if (e.MessageId >= 0x1000&&e.MessageId < 0x2000)
            {
                try
                {
                    Debug.WriteLine($"JsCallNative:{e.MessageId},{e.CallInfoJson}");
                    var invokeInfo = JsonSerializer.Deserialize<InvokeUIServiceInfo>(e.CallInfoJson);
                    if (invokeInfo == null ||
                        string.IsNullOrEmpty(invokeInfo.ServiceName) ||
                        string.IsNullOrEmpty(invokeInfo.MethodName) ||
                        invokeInfo.Arguments == null
                    )
                    {
                        throw new Exception($"协议错误，CallInfoJson：{e.CallInfoJson}。");
                    }
                    if (!UIServices.ContainsKey(invokeInfo.ServiceName))
                    {
                        throw new Exception($"服务没找到；{invokeInfo.ServiceName}。");
                    }
                    object service = UIServices[invokeInfo.ServiceName];                    
                    var methodInfo = service.GetType().GetMethod(invokeInfo.MethodName, BindingFlags.Public | BindingFlags.Instance);
                    if (methodInfo == null)
                    {
                        throw new Exception($"方法没找到：{invokeInfo.ServiceName}.{invokeInfo.MethodName}。");
                    }
                    object? result = null;
                    List<object?> args = new List<object?>();
                    var parameters = methodInfo.GetParameters();
                    for (var i = 0; i < invokeInfo.Arguments.Count; i++)
                    {
                        args.Add(JsonElementToObject(parameters[i].ParameterType, (JsonElement)invokeInfo.Arguments[i]));
                    }
                    if (methodInfo.ReturnType != typeof(void))
                    {
                        if (!methodInfo.ReturnType.FullName.StartsWith("System.Threading.Tasks.Task"))
                        {
                            result = methodInfo.Invoke(service, args.ToArray());
                        }
                        else
                        {
                            result = methodInfo.Invoke(service, args.ToArray());
                            if (result != null)
                            {
                                dynamic dobj = result;
                                if (dobj.Exception != null)
                                {
                                    throw dobj.Exception;
                                }
                                var resultType = result.GetType();
                                if (resultType.Name.StartsWith("AsyncStateMachineBox`"))
                                {
                                    if (resultType.GenericTypeArguments.Length > 0 && resultType.GenericTypeArguments[0].Name == "VoidTaskResult")
                                    {
                                        await dobj;
                                    }
                                    else
                                    {
                                        result = await dobj;
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        methodInfo.Invoke(service, args.ToArray());
                    }
                    e.Result = new { Status = true, Value = JsonSerializer.Serialize(result) };
                }
                catch (Exception ex)
                {
                    e.Result = new { Status = false, Value = ex.GetBaseException().ToString() };
                }
            }
        }

        public static Stream UseHandler(string url)
        {
            if(url.ToLower().StartsWith("use://script/webapp.js?"))
            {                
                var tmpStrs = url.Split('?')[1].Split('&');
                var name = tmpStrs[0].Split('=')[1];
                var webViewPath= tmpStrs[1].Split('=')[1];
                var outStream= new MemoryStream();

                using (var stream = WebApplication.GetManifestResource("Tnelab.Common/Tnelab.Common.js"))
                {
                    var buffer = new byte[stream.Length + 2];
                    buffer[stream.Length] = (byte)'\r';
                    buffer[stream.Length + 1] = (byte)'\r';
                    stream.Read(buffer, 0, (int)stream.Length);
                    outStream.Write(buffer, 0, buffer.Length);
                }

                using (var stream = WebApplication.GetManifestResource($"{webViewPath}/WebAppQuery.js"))
                {
                    var buffer= new byte[stream.Length+2];
                    buffer[stream.Length] = (byte)'\r';
                    buffer[stream.Length+1] = (byte)'\r';
                    stream.Read(buffer, 0, (int)stream.Length);
                    outStream.Write(buffer, 0, buffer.Length);
                }

                using (var stream = WebApplication.GetManifestResource("Tnelab.WebApp/WebAppCallNative.js"))
                {
                    var buffer = new byte[stream.Length + 2];
                    buffer[stream.Length] = (byte)'\r';
                    buffer[stream.Length + 1] = (byte)'\r';
                    stream.Read(buffer, 0, (int)stream.Length);
                    outStream.Write(buffer, 0, buffer.Length);
                }
                using (var stream = WebApplication.GetManifestResource("Tnelab.WebApp/InvokeUIService.js"))
                {
                    var buffer = new byte[stream.Length + 2];
                    buffer[stream.Length] = (byte)'\r';
                    buffer[stream.Length + 1] = (byte)'\r';
                    stream.Read(buffer, 0, (int)stream.Length);
                    outStream.Write(buffer, 0, buffer.Length);
                }

                using (var stream = WebApplication.UIServiceHandler($"UIService://{name}?Name=ThisWindow"))
                {
                    var buffer = new byte[stream.Length + 2];
                    buffer[stream.Length] = (byte)'\r';
                    buffer[stream.Length + 1] = (byte)'\r';
                    stream.Read(buffer, 0, (int)stream.Length);
                    outStream.Write(buffer, 0, buffer.Length);
                }

                using (var stream = WebApplication.GetManifestResource("Tnelab.WebApp/InitWebWindow.js"))
                {
                    var buffer = new byte[stream.Length + 2];
                    buffer[stream.Length] = (byte)'\r';
                    buffer[stream.Length + 1] = (byte)'\r';
                    stream.Read(buffer, 0, (int)stream.Length);
                    outStream.Write(buffer, 0, buffer.Length);
                }
                outStream.Position= 0;
                return outStream;
            }
            throw new Exception($"{url} not found.");
        }

        public static Stream UIServiceHandler(string url)
        {
            var uri=new Uri(url);
            var tmpStr = url.Split('?');
            var serviceName = tmpStr[0].Remove(0, "uiservice://".Length);
            var name = serviceName;
            if(tmpStr.Length > 1)
            {
                tmpStr = tmpStr[1].Split('&');
            }
            for(var i = 0; i < tmpStr.Length; i++)
            {
                if (tmpStr[i].StartsWith("Name=")){
                    tmpStr = tmpStr[i].Split('=');
                    name = tmpStr[1];
                }
            }
            if (!UIServices.ContainsKey(serviceName))
            {
                throw new Exception($"{url} not found");
            }
            var service = UIServices[serviceName];
            var serviceType = service.GetType();
            var methods = serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            var scriptBuilder = new StringBuilder();
            var scriptCode = serviceType.GetCustomAttribute<ScriptCodeAttribute>();
            if (scriptCode != null)
            {
                scriptBuilder.AppendLine(scriptCode.Code);
            }
            scriptBuilder.AppendLine($"class _{serviceName}");
            scriptBuilder.AppendLine("{");
            scriptBuilder.AppendLine("  constructor() {}");
            Dictionary<string, bool> scriptCodeStatusDic = new Dictionary<string, bool>();
            foreach (var method in methods)
            {
                if(method.GetCustomAttribute<DoNotConvertToScriptAttribute>()!=null)
                {
                    continue;
                }
                if (method.IsSpecialName)
                {
                    var property = serviceType.GetProperty(method.Name.Remove(0, 4));
                    if(property==null)
                    {
                        continue;
                    }
                    scriptCode = property.GetCustomAttribute<ScriptCodeAttribute>();
                    if (scriptCode != null)
                    {
                        scriptCode.Context = service;
                        scriptBuilder.Append($"  {method.Name.Substring(0, 3)} _{method.Name.Remove(0, 4)}");
                    }
                    else
                    {
                        scriptBuilder.Append($"  {method.Name.Substring(0, 3)} {method.Name.Remove(0, 4)}");
                    }                    
                    if (method.Name.StartsWith("set_"))
                    {
                        scriptBuilder.Append($"(val){{ return InvokeUIService({{ServiceName:\"{serviceName}\",MethodName:\"{method.Name}\",Arguments:[val]}});");
                    }
                    else if (method.Name.StartsWith("get_"))
                    {
                        scriptBuilder.Append($"(){{ return InvokeUIService({{ServiceName:\"{serviceName}\",MethodName:\"{method.Name}\",Arguments:[]}});");
                    }
                    scriptBuilder.AppendLine("}");
                    if (scriptCode != null&& !scriptCodeStatusDic.ContainsKey(property.Name))
                    {
                        scriptBuilder.AppendLine(scriptCode.Code);
                        scriptCodeStatusDic.Add(property.Name, true);
                    }
                }
                else
                {
                    scriptCode=method.GetCustomAttribute<ScriptCodeAttribute>();
                    if (scriptCode != null)
                    {
                        scriptBuilder.Append($"  async _{method.Name}(");
                    }
                    else
                    {
                        scriptBuilder.Append($"  async {method.Name}(");
                    }
                    var parameters = method.GetParameters();
                    var argsStr = String.Join(',', parameters.Select(it => it.Name));
                    scriptBuilder.Append($"{argsStr}");
                    scriptBuilder.AppendLine("){");
                    var returnStr = "";
                    if(method.ReturnType != typeof(void))
                    {
                        returnStr = "return ";
                    }
                    scriptBuilder.AppendLine($"      {returnStr}await InvokeUIService({{ServiceName:\"{serviceName}\",MethodName:\"{method.Name}\",Arguments:[{argsStr}]}});");
                    scriptBuilder.AppendLine("  }");
                    if (scriptCode != null)
                    {
                        scriptBuilder.AppendLine(scriptCode.Code);
                    }
                }
            }
            var events = serviceType.GetEvents();
            if(events.Length > 0)
            {
                scriptBuilder.AppendLine("  _WebAppEventList=[];");
                scriptBuilder.AppendLine("  AddEventListener(evtName,handler){");
                scriptBuilder.AppendLine("      this._WebAppEventList.push({ Name: evtName, Handler: handler });");
                scriptBuilder.AppendLine("  }");
                scriptBuilder.AppendLine("  RemoveEventListener(evtName,handler){");
                scriptBuilder.AppendLine("      for(let i=0;i<this._WebAppEventList.length;i++){");
                scriptBuilder.AppendLine("          if(this._WebAppEventList[i].Name===evtName&&this._WebAppEventList[i].Handler===handler){");
                scriptBuilder.AppendLine("              this._WebAppEventList.splice(i, 1);");
                scriptBuilder.AppendLine("              break;");
                scriptBuilder.AppendLine("          }");                
                scriptBuilder.AppendLine("      }");
                scriptBuilder.AppendLine("  }");
            }
            void EvtHandler(object sender, object args,string name,string evtName)
            {
                var script = @"
                        for(let i=0;i<{name}._WebAppEventList.length;i++){
                            if({name}._WebAppEventList[i].Name==""{evtName}""){
                                {name}._WebAppEventList[i].Handler({args});
                            }
                        }
                    ";
                script = script.Replace("{name}", name);
                script = script.Replace("{evtName}", evtName);
                var json = JsonSerializer.Serialize(args);
                script = script.Replace("{args}", json);
                var uiService = sender as IUIService;
                uiService.WebBrowser.CallJsAsync(script);
            };
            foreach (var evt in events)
            {
                var getEvtHandlerDelegate = () =>
                {
                    //定义参数
                    var paramTypeList = new List<Type>();
                    paramTypeList.AddRange(evt.EventHandlerType.GetMethod("Invoke").GetParameters().Select(it => it.ParameterType).ToArray());
                    var parameExps = new List<ParameterExpression>();
                    parameExps.Add(Expression.Parameter(paramTypeList[0], "sender"));
                    parameExps.Add(Expression.Parameter(paramTypeList[1], "args"));
                    //调用EvtHandler
                    var argsExps = new List<Expression>();
                    argsExps.Add(parameExps[0]);
                    argsExps.Add(Expression.TypeAs(parameExps[1], typeof(object)));
                    argsExps.Add(Expression.Constant(name));
                    argsExps.Add(Expression.Constant(evt.Name));
                    var invokeEvtHandler = Expression.Invoke(Expression.Constant(EvtHandler), argsExps);
                    return Expression.Lambda(evt.EventHandlerType, invokeEvtHandler, parameExps).Compile() as Delegate;
                };
                evt.AddEventHandler(service, getEvtHandlerDelegate());
            }
            scriptBuilder.AppendLine("}");
            scriptBuilder.AppendLine($"let {name} = new _{serviceName}();");
            var script = scriptBuilder.ToString();
            var buffer = Encoding.UTF8.GetBytes(script);
            return new MemoryStream(buffer);
        }
        public static Stream RemoteServiceHandler(string url)
        {
            var flag = "service://";
            var pre = url.Split('=')[1];
            url = url.Remove(0, flag.Length);
            url = url.Split('?')[0];
            url = url.Replace('/', ',');
            url = $"{pre}/GetService?Path={url}";
            using var httpClient= new HttpClient();
            var datas=httpClient.GetByteArrayAsync(url).GetAwaiter().GetResult();
            return new MemoryStream(datas);
        }
        public static Stream GetManifestResource(string path)
        {
            var assemblyName = path.Substring(0, path.IndexOf('/'));
            var resPath = path.ToLower().Replace('/', '.');
            var assembly = Assembly.Load(assemblyName);
            var names = assembly.GetManifestResourceNames();
            foreach (var name in names)
            {
                if (name.ToLower() == resPath)
                {
                    var stream = assembly.GetManifestResourceStream(name);
                    if (stream != null)
                    {
                        return stream;
                    }
                }
            }
            throw new Exception($"找不到嵌入的资源:{path}");
        }
    }
}